package sjtu.pirate.httpclient;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.joda.time.DateTime;

import sjtu.pirate.format.Form;
import sjtu.pirate.format.FormResult;
import sjtu.pirate.format.Input;
import sjtu.pirate.format.Form.FormMethod;
/**
 * @author wilbur.ma@qq.com
 * @date 2011.05.26
 * Wrapper class for apache httpclient.
 */
public class PirateClient {
	private HttpClient client;
	private String encoding;
	
	public PirateClient() {
		this(HTTP.UTF_8, 0);
	}
	
	public PirateClient(int timeout) {
		this(HTTP.UTF_8, timeout);
	}
	
	public PirateClient(String encoding, int timeout) {
		this(encoding, timeout, null);
	}
	
	public PirateClient(String encoding, int timeout, URI proxyURI) {
		SchemeRegistry schemeRegistry = new SchemeRegistry();
		schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
		schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));
		ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
		// Increase max total connection to 200
		cm.setMaxTotal(200);
		// Increase default max connection per route to 20
		cm.setDefaultMaxPerRoute(20);
		this.client = new DefaultHttpClient(cm);
		HttpParams params = client.getParams();
		HttpConnectionParams.setConnectionTimeout(params, timeout);
		if (proxyURI != null) {
			HttpHost proxy = new HttpHost(proxyURI.getHost(), proxyURI.getPort(), proxyURI.getScheme());
			params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
		}
		this.encoding = encoding;
	}
	
	public HttpContext sendRequest(URI uri, String method, List<NameValuePair> params) 
			throws ClientProtocolException,	URISyntaxException, IOException {
		/* init request object */
		HttpContext context = new BasicHttpContext();
		HttpRequestBase request = null;
		if (method.equals(HttpGet.METHOD_NAME)) {
			// get
			request = new HttpGet(uri);
		} else {
			// post
			request = new HttpPost(uri);
			UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, encoding);
			((HttpPost) request).setEntity(entity);
		}
		/* send request */
		try {
			client.execute(request, context);
		} catch (IOException e) {
			e.printStackTrace();
			throw e;
		} finally {
			closeExpiredConnections();
		}
		return context;
	}
	
	/**
	 * Send a form using its method, return its context.
	 */
	public FormResult sendRequest(Form form) 
			throws ClientProtocolException, URISyntaxException, IOException {
		/* reset cookies */
		resetCookies(form.getURI().getHost());
		sendSimpleRequest(form);
		Hashtable<String, Cookie> oldCookies = findCookies(form.getURI().getHost());
		/* send request and generate FormResult */
		List<NameValuePair> params = buildParams(form);
		URI uri = null;
		if (form.getMethod() == FormMethod.GET)
			uri = buildFormActionURI(form, URLEncodedUtils.format(params, encoding));
		else
			uri = form.getActionURI();
		DateTime start = new DateTime();
		HttpContext context = sendRequest(uri, form.getMethod().toString(), params);
		DateTime end = new DateTime();
		HttpResponse response = (HttpResponse) context.getAttribute("http.response");
		FormResult fr = new FormResult(form, isLogin(context, oldCookies), response.getStatusLine().getStatusCode(), start, end);
		return fr;
	}
	
	/**
	 * Send a request using get method in order to update cookies of the host.
	 */
	public void sendSimpleRequest(Form form) 
			throws URISyntaxException, ClientProtocolException, IOException {
		HttpGet httppost = new HttpGet(form.getActionURI());
		client.execute(httppost);
	}
	
	/**
	 * Shutdown client connection;
	 */
	public void shutdown() {
		client.getConnectionManager().shutdown();
	}
	
	/**
	 * Close expired connections in client manager.
	 */
	public void closeExpiredConnections() {
		client.getConnectionManager().closeExpiredConnections();
	}
	
	/**
	 * If login fail, cookies of the host should not change.
	 */
	public boolean isLogin(HttpContext context, Hashtable<String, Cookie> oldCookies) {
		if (context == null)
			return false;
		HttpResponse response = (HttpResponse) context.getAttribute("http.response");
		if (response == null)
			return false;
//		if (response.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY)
//			return true;
		HttpHost host = (HttpHost) context.getAttribute("http.target_host");
		Hashtable<String, Cookie> cookies = findCookies(host.getHostName());
		if (!isCookiesEqual(cookies, oldCookies))
			return true;
		return false;
	}
	
	/**
	 * Get cookies specified by domain from httpclient cookiestore.
	 */
	private Hashtable<String, Cookie> findCookies(String host) {
		Hashtable<String, Cookie> res = new Hashtable<String, Cookie>();
		List<Cookie> cookies = ((DefaultHttpClient) client).getCookieStore().getCookies();
		for (Cookie c : cookies) {
			if (isDomainEqual(c.getDomain(), host))
				res.put(c.getDomain() + ":" + c.getName(), c);
		}
		return res;
	}
	
	/**
	 * Reset cookies of the host.
	 */
	private void resetCookies(String domain) {
		for (Cookie c : findCookies(domain).values()) {
			if (c != null)
				c = null;
		}
	}
	
	/**
	 * Generate a list of NameValuePair{input.name, input.value}.
	 */
	public static List<NameValuePair> buildParams(Form form) {
		List<NameValuePair> qparams = new ArrayList<NameValuePair>(0);;
		if (form == null || form.getInputList() == null)
			return qparams;
		else {
			List<Input> list = form.getInputList();
			qparams = new ArrayList<NameValuePair>(list.size());
			for (Input inp : form.getInputList()) {
				qparams.add(new BasicNameValuePair(inp.getName(), inp.getValue()));
			}
		}
		return qparams;
	}
	
	/**
	 * Build the action uri of form, taking the form path and form action as input.
	 */
	public static URI buildFormActionURI(Form form, String query)
			throws URISyntaxException {
		URI uri = form.getActionURI();
		return URIUtils.createURI(uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath(), query, null);
	}
	
	/**
	 * Compare two domains.
	 * (.baidu.com, www.baidu.com) evals true.
	 */
	public static boolean isDomainEqual(String domain, String domain2) {
		if (domain.charAt(0) == '.')
			domain =  "www" + domain;
		if (domain2.charAt(0) == '.')
			domain2 = "www" + domain2;
		return domain.equalsIgnoreCase(domain2);
	}
	
	/**
	 * Compare cookies.
	 */
	public static boolean isCookiesEqual(Hashtable<String, Cookie> table, Hashtable<String, Cookie> table2) {
		if (table == null || table2 == null)
			return false;
		if (table.size() != table2.size())
			return false;
		for (String k : table.keySet()) {
			Cookie c = table.get(k);
			Cookie c2 = table2.get(k);
			if (c == null || c2 == null)
				return false;
			if (!c.getValue().equals(c2.getValue()))
				return false;
		}
		return true;
	}
	
	/**
	 * Print entity info.
	 */
	public static void printEntity(HttpEntity entity, String encoding) 
			throws IllegalStateException, IOException {
		if (entity == null) {
			System.out.println("entity is null.");
			return;
		}
		System.out.println("Response content length: "
				+ entity.getContentLength());
		InputStream is = entity.getContent();
//		BufferedReader in = new BufferedReader(new InputStreamReader(is));
		List<Byte> li = new ArrayList<Byte>();
		int i;

		while ((i = is.read()) != -1) {
			li.add((byte) i);
		}
		byte a[] = new byte[li.size()];
		for (i = 0; i < a.length; i++) {
			a[i] = (byte) li.get(i);
		}
		if (encoding == null || encoding.equals(""))
			encoding = HTTP.UTF_8;
		System.out.println(new String(a, encoding));
	}
}